lst-ch31-local-panel 至季度前向调参和最终时间外测试,确保 shift 不跨公司。scikit-learn 构建KNN分类模型KNN 是一种经典的监督学习分类算法
方法可拓展应用的应用(业务对象与标签必须重新验证):
决策规则:
关键问题:如何衡量’距离’?
最常用的距离度量是欧氏距离(Euclidean Distance):
\[ \large d(x,y) = \sqrt{\sum_{i=1}^{n}(x_i - y_i)^2} \]
直观理解:就是多维空间中两点之间的直线距离
| K值过小 | K值过大 |
|---|---|
| 模型过拟合 | 模型欠拟合 |
| 对噪声敏感 | 决策边界过于平滑 |
| 分类不稳定 | 丢失局部特征 |
最佳实践:通过交叉验证自动搜索最优K值
对照边界:
数据特征:
| 特征 | 说明 |
|---|---|
| Age | 用户年龄 |
| EstimatedSalary | 预估年薪 |
| Purchased | 是否购买(0/1) |
data、x、y、transfer1 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。# 注:04_Social_Network_Ads.csv数据文件本地没有,但平台已经内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
# 导入相关模块
import pandas as pd
import numpy as np # 导入NumPy数值计算库
from sklearn.model_selection import train_test_split # 导入Scikit-learn的train_test_split模块
from sklearn.preprocessing import StandardScaler # 导入Scikit-learn的StandardScaler模块
from sklearn.neighbors import KNeighborsClassifier # 导入Scikit-learn的KNeighborsClassifier模块
from sklearn.model_selection import GridSearchCV # 导入Scikit-learn的GridSearchCV模块
from sklearn.metrics import confusion_matrix # 导入Scikit-learn的confusion_matrix模块
# 导入数据集
data = pd.read_csv("04_Social_Network_Ads.csv")
# 数据集划分
x = data[["Age","EstimatedSalary"]]
y = data["Purchased"] # 提取Purchased列作为y变量
# 划分训练集和测试集
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.30, random_state=0)
# 数据标准化
transfer1 = StandardScaler()
x_train = transfer1.fit_transform(x_train) # 对数据进行变换
x_test = transfer1.transform(x_test) # 对数据进行变换
# 训练模型
estimator = KNeighborsClassifier(algorithm='kd_tree')
# 模型选择与调优——网格搜索和交叉验证
# 准备要调的超参数
param_dict = {"n_neighbors": [1, 3, 5, 7, 9, 11, 13]}
estimator = GridSearchCV(estimator, param_grid=param_dict, cv=4) # 创建网格搜索交叉验证对象,自动寻找最优超参数
estimator.fit(x_train,y_train) # 在数据上训练estimator模型
# 模型评估
y_pre = estimator.predict(x_test)
print("预测结果:\n", y_pre) # 输出预测结果:\n
print("准确率为:\n", estimator.score(x_test, y_test)) # 输出准确率为:\n
# print("对比真实值和预测值:\n", y_test==y_pre)
print("在交叉验证中最好的结果为:\n", estimator.best_score_)
print("使用网格搜索的最好的模型:\n", estimator.best_estimator_) # 输出使用网格搜索的最好的模型:\n运行后核对:核对 data、x、y、transfer1 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。
拓展练习:把对象或期间改为一家长三角上市公司或一组 A 股资产;先预测指标方向,再说明结果能支持和不能支持的决策。
数据导入:使用 pd.read_csv() 读取CSV文件
特征与标签分离:
x:Age 和 EstimatedSalary(特征矩阵)y:Purchased(目标变量)数据集划分:
random_state=0 保证每次运行都能得到一致结果为什么需要标准化?
StandardScaler 的作用:
fit_transform:在训练集上拟合并转换transform:用训练集的参数转换测试集(避免数据泄露)GridSearchCV 自动完成超参数调优:
平台数据 04_Social_Network_Ads.csv 仅用于教学平台固定练习;下方使用本地 A 股数据完整演示同一分析方法。
评估指标:
分类错误的业务代价并不对称(FP 与 FN 各自的代价因任务而异),下方真实数据处理过程会结合 TTM 股息率状态核对任务给出具体解读。
| 预测:TTM股息率非正(0) | 预测:TTM股息率为正(1) | |
|---|---|---|
| 实际:TTM股息率非正(0) | TN(正确排除) | FP(误纳入状态核对队列) |
| 实际:TTM股息率为正(1) | FN(漏掉核对对象) | TP(正确纳入) |
| 要素 | 课堂口径 |
|---|---|
| 本地资产 | valuation_factors_quarterly_15_years.h5 |
| 样本/标签 | 2019Q3—2025Q3 日历对齐观测;标签=下一季度TTM股息率>0 |
| 特征 | 对数市值、市净率、市销率、盈利收益率 |
| 划分 | 有效训练至2023Q1;样本外自2024Q3(面板有季度缺口) |
| 调优/参照 | 训练期内按季度扩展窗前向验证;最终时间外多数类基线 |
3pp。85%。85%。2026-09-30。from pathlib import Path # 导入路径工具以探测课程数据挂载
import numpy as np # 导入数值计算库
import pandas as pd # 导入数据分析库
from sklearn.preprocessing import StandardScaler # 导入标准化器
from sklearn.neighbors import KNeighborsClassifier # 导入KNN分类器
from sklearn.model_selection import GridSearchCV # 导入网格搜索并接收自定义季度前向折
from sklearn.metrics import confusion_matrix, accuracy_score, recall_score, precision_score # 导入评估指标
from sklearn.pipeline import Pipeline # 导入管道以便在每个交叉验证训练折内拟合预处理
from sklearn.preprocessing import FunctionTransformer # 导入无状态变换器以执行标准化后截尾
candidate_roots = [Path('/home/ubuntu/r2_data_mount/data'), Path(r'C:\qiufei\data')] # 按课程规定顺序探测数据根目录
data_root = next((root for root in candidate_roots if root.is_dir()), None) # 选择第一个可用根目录
factor_path = data_root / 'stock' / 'valuation_factors_quarterly_15_years.h5' if data_root else None # 定位估值因子季度表
if factor_path is None or not factor_path.is_file(): # 真实数据缺失时明确提示
raise FileNotFoundError(f'未找到估值因子文件:{factor_path};请从课程数据下载入口获取') # 提示读者下载文件valuation_panel = pd.read_hdf(factor_path, key='valuation_factors') # 读取真实A股估值因子面板
feature_columns = ['market_cap', 'pb_ratio_lf', 'ps_ratio_ttm', 'ep_ratio_ttm'] # 规模与估值四特征
dividend_panel = valuation_panel[feature_columns + ['dividend_yield_ttm']].replace([np.inf, -np.inf], np.nan).dropna() # 去除缺失与无穷值
dividend_panel = dividend_panel.reset_index().sort_values(['order_book_id', 'date']) # 展开索引并按公司与报告期排序
calendar_quarters = pd.date_range(dividend_panel['date'].min(), dividend_panel['date'].max(), freq='QE') # 构建完整日历季度网格
company_frames = [] # 收集每公司按日历重索引的结果
for company_code, company_frame in dividend_panel.groupby('order_book_id'): # 逐公司处理
reindexed_frame = company_frame.set_index('date').drop(columns='order_book_id').reindex(calendar_quarters) # 把数据缺口显式补为缺失行
reindexed_frame['order_book_id'] = company_code # 回填公司代码
company_frames.append(reindexed_frame.reset_index().rename(columns={'index': 'date'})) # 收集结果
dividend_panel = pd.concat(company_frames, ignore_index=True) # 合并为日历对齐面板
print(f'日历网格:{len(calendar_quarters)}个季度|覆盖公司:{dividend_panel["order_book_id"].nunique()}家') # 报告日历对齐输入日历网格:60个季度|覆盖公司:5157家
next_quarter_yield = dividend_panel.groupby('order_book_id')['dividend_yield_ttm'].shift(-1) # 同一公司下一日历季度TTM股息率
dividend_panel['next_ttm_yield_positive'] = next_quarter_yield > 0 # 标签:下一季度TTM股息率是否大于0
dividend_panel = dividend_panel[next_quarter_yield.notna()].dropna(subset=feature_columns) # 仅保留标签与特征完整的行
classroom_panel = dividend_panel[dividend_panel['date'] >= '2019-01-01'].copy() # 保留2019年起的课堂样本
classroom_panel['log_market_cap'] = np.log(classroom_panel['market_cap']) # 市值对数化压缩尺度
print(f'输入口径:{len(classroom_panel)}个公司—季度样本({classroom_panel["date"].min().date()}—{classroom_panel["date"].max().date()})|下一季度TTM股息率为正占比{classroom_panel["next_ttm_yield_positive"].mean():.1%}') # 报告样本与标签分布输入口径:76568个公司—季度样本(2019-09-30—2025-09-30)|下一季度TTM股息率为正占比76.4%
model_features = ['log_market_cap', 'pb_ratio_lf', 'ps_ratio_ttm', 'ep_ratio_ttm'] # 模型输入特征
train_mask = classroom_panel['date'] <= '2023-12-31' # 名义训练期≤2023Q4;因面板季度缺失,有效标签止于2023Q1
test_mask = classroom_panel['date'] >= '2024-01-01' # 名义测试期≥2024Q1;有效样本外观测自2024Q3起
x_train_raw = classroom_panel.loc[train_mask, model_features] # 训练特征(标准化前)
x_test_raw = classroom_panel.loc[test_mask, model_features] # 测试特征(标准化前)
y_train = classroom_panel.loc[train_mask, 'next_ttm_yield_positive'] # 提取训练期TTM股息率状态标签
y_test = classroom_panel.loc[test_mask, 'next_ttm_yield_positive'] # 提取样本外TTM股息率状态标签
majority_baseline = max(y_test.mean(), 1 - y_test.mean()) # 多数类基线准确率
print(f'时间划分:训练{len(y_train)}条(有效至2023Q1)|测试{len(y_test)}条(自2024Q3)|多数类基线{majority_baseline:.3f}|标准化将在每个训练折内拟合') # 报告有效划分、基线与无泄漏口径时间划分:训练51316条(有效至2023Q1)|测试25252条(自2024Q3)|多数类基线0.759|标准化将在每个训练折内拟合
clip_transformer = FunctionTransformer(np.clip, kw_args={'a_min': -5, 'a_max': 5}) # 将标准化后的极端值截到正负5且不估计额外参数
knn_pipeline = Pipeline([('scale', StandardScaler()), ('clip', clip_transformer), ('knn', KNeighborsClassifier())]) # 把标准化、截尾与KNN封装为同一管道
k_param_grid = {'knn__n_neighbors': [3, 5, 7, 9, 11, 13]} # 使用管道参数名扫描候选K值
training_dates = classroom_panel.loc[train_mask, 'date'].reset_index(drop=True) # 保存每条训练样本的季度键
candidate_validation_dates = sorted(training_dates.unique())[-4:] # 取训练期末四个可用季度作前向验证块
forward_splits = [] # 收集严格早期训练到后期验证的索引对
for validation_date in candidate_validation_dates: # 逐季度建立扩展窗验证折
fold_train = np.flatnonzero(training_dates.lt(validation_date).to_numpy()) # 只允许更早季度进入折内训练
fold_valid = np.flatnonzero(training_dates.eq(validation_date).to_numpy()) # 同一季度公司共同进入验证块
if len(fold_train) and len(fold_valid): forward_splits.append((fold_train, fold_valid)) # 保存有效时间折
if len(forward_splits) < 3: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 前向折不足时停止调参
grid_search = GridSearchCV(knn_pipeline, param_grid=k_param_grid, cv=forward_splits, n_jobs=1) # 每折只用更早季度拟合预处理与KNN
grid_search.fit(x_train_raw.reset_index(drop=True), y_train.reset_index(drop=True)) # 最终时间外测试集不参与调优
print(f'最优K={grid_search.best_params_["knn__n_neighbors"]}|季度前向{len(forward_splits)}折准确率{grid_search.best_score_:.3f}') # 展示前向调优结果最优K=13|季度前向4折准确率0.821
复算要求:旧的按公司随机分折可能用较晚季度验证较早季度,不再作为无时间泄漏依据。必须重新运行季度前向折并报告每折训练截止/验证季度;未执行时不预填最优K、CV分数或最终判断条件。
y_predict = grid_search.predict(x_test_raw) # 管道用训练期参数转换并预测样本外TTM股息率状态
confusion_table = confusion_matrix(y_test, y_predict) # 混淆矩阵(行=实际,列=预测)
test_accuracy = accuracy_score(y_test, y_predict) # 样本外准确率
positive_status_recall = recall_score(y_test, y_predict) # 正状态召回率:真实TTM股息率为正中被找出的比例
positive_status_precision = precision_score(y_test, y_predict) # 正状态精度:预测为正中真实为正的比例
improvement_pp = (test_accuracy - majority_baseline) * 100 # 相对基线的提升幅度(百分点)
margin_limit, recall_limit, precision_limit = 3.0, 0.85, 0.85 # 预声明的三条检查要求阈值
conditions_met = improvement_pp >= margin_limit and positive_status_recall >= recall_limit and positive_status_precision >= precision_limit # 三条件同时满足才通过
decision = '允许生成课堂TTM状态核对队列' if conditions_met else '检查要求未通过,暂停状态核对队列输出' # 课堂建议
print(f'混淆矩阵[[TN,FP],[FN,TP]]:{confusion_table.tolist()}') # 展示分类明细
print(f'样本外准确率{test_accuracy:.3f}(基线{majority_baseline:.3f},+{improvement_pp:.1f}pp)|正状态召回{positive_status_recall:.1%}|正状态精度{positive_status_precision:.1%}') # 展示验证指标
print(f'检查要求(优于基线≥{margin_limit:.0f}pp、召回≥{recall_limit:.0%}、精度≥{precision_limit:.0%}):{"通过" if conditions_met else "暂停"}|{decision}') # 展示检查要求与条件动作混淆矩阵[[TN,FP],[FN,TP]]:[[4076, 2020], [2048, 17108]]
样本外准确率0.839(基线0.759,+8.0pp)|正状态召回89.3%|正状态精度89.4%
检查要求(优于基线≥3pp、召回≥85%、精度≥85%):通过|允许生成课堂TTM状态核对队列
| 步骤 | 关键要点 |
|---|---|
| 划分 | 平台70/30随机划分;真实数据分析按时间有序划分 |
| 标准化 | 只用训练集拟合,防尺度主导与泄露 |
| 调优 | 训练期内按季度扩展窗前向验证搜索K值 |
| 评估 | 准确率+混淆矩阵,对比多数类基线 |
核心收获:特征标准化、K值选择和样本外验证是成败关键;结论须待预声明判断条件通过。
lst-ch31-local-panel 至时间切分,写出公司—季度标签对齐、各前向折训练截止/验证季度、最终训练/测试期间、最优K、混淆矩阵及多数类基线;抽一家公司核对 shift(-1) 没有跨主体。shift(-1) 不跨主体检查。valuation_factors_quarterly_15_years.h5:valuation_factors;公司—季度键,特征 market_cap, pb_ratio_lf, ps_ratio_ttm, ep_ratio_ttm;标签为同一公司下一日历季度 dividend_yield_ttm>0,不是下一季度现金分红事件。lst-ch31-local-panel 至 lst-ch31-local-split 得到原始训练/测试表,再执行无泄漏管道:from pathlib import Path # 导入路径工具以定位规定估值因子快照
import numpy as np # 导入数值工具以处理无穷值与对数市值
import pandas as pd # 导入表格工具以对齐公司日历季度
factor_path=Path('/home/ubuntu/r2_data_mount/data/stock/valuation_factors_quarterly_15_years.h5') # 绑定真实季度估值因子面板
if not factor_path.exists(): raise FileNotFoundError('未找到课程数据文件,请从课程数据下载入口获取并核对文件位置') # 规定因子资产缺失时终止
valuation_panel=pd.read_hdf(factor_path,key='valuation_factors') # 读取公司季度估值因子面板
feature_columns=['market_cap','pb_ratio_lf','ps_ratio_ttm','ep_ratio_ttm'] # 固定规模与估值四个预测特征
dividend_panel=valuation_panel[feature_columns+['dividend_yield_ttm']].replace([np.inf,-np.inf],np.nan).dropna().reset_index().sort_values(['order_book_id','date']) # 去除非有限值并展开公司季度键
calendar_quarters=pd.date_range(dividend_panel['date'].min(),dividend_panel['date'].max(),freq='QE') # 构造完整日历季度网格
company_frames=[] # 初始化逐公司日历对齐结果
for company_code,company_frame in dividend_panel.groupby('order_book_id'): # 逐公司隔离时序标签
reindexed_frame=company_frame.set_index('date').drop(columns='order_book_id').reindex(calendar_quarters) # 将缺失季度显式补为空行
reindexed_frame['order_book_id']=company_code # 回填公司代码避免跨主体移位
company_frames.append(reindexed_frame.reset_index().rename(columns={'index':'date'})) # 收集日历对齐的公司面板
dividend_panel=pd.concat(company_frames,ignore_index=True) # 合并全部公司的日历季度表next_quarter_yield=dividend_panel.groupby('order_book_id')['dividend_yield_ttm'].shift(-1) # 仅在同一公司内取下一日历季度股息率
dividend_panel['next_ttm_yield_positive']=next_quarter_yield.gt(0) # 构造下一季度TTM股息率大于零的状态标签
dividend_panel=dividend_panel.loc[next_quarter_yield.notna()].dropna(subset=feature_columns) # 仅保留标签与特征完整样本
classroom_panel=dividend_panel.loc[dividend_panel['date'].ge('2019-01-01')].copy() # 固定2019年起的课堂样本期
classroom_panel['log_market_cap']=np.log(classroom_panel['market_cap']) # 对数化市值以缓和尺度偏斜
model_features=['log_market_cap','pb_ratio_lf','ps_ratio_ttm','ep_ratio_ttm'] # 固定进入KNN管道的特征顺序
train_mask=classroom_panel['date'].le('2023-12-31') # 锁定2023年末前为训练期
test_mask=classroom_panel['date'].ge('2024-01-01') # 锁定2024年起为时间外测试期
x_train_raw=classroom_panel.loc[train_mask,model_features] # 提取未标准化的训练特征
x_test_raw=classroom_panel.loc[test_mask,model_features] # 提取未标准化的样本外特征
y_train=classroom_panel.loc[train_mask,'next_ttm_yield_positive'] # 提取训练期下一季度TTM状态标签
y_test=classroom_panel.loc[test_mask,'next_ttm_yield_positive'] # 提取样本外下一季度TTM状态标签
if y_train.empty or y_test.empty or y_test.nunique()<2: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 训练测试或测试类别不完整时终止from sklearn.pipeline import Pipeline # 导入管道以封装无泄漏预处理与模型
from sklearn.preprocessing import StandardScaler # 导入标准化器以仅在训练样本拟合尺度参数
from sklearn.neighbors import KNeighborsClassifier # 导入K近邻分类器以执行距离投票
from sklearn.model_selection import GridSearchCV # 导入网格搜索并使用自定义季度前向折
from sklearn.metrics import confusion_matrix,accuracy_score,precision_score,recall_score # 导入分类或聚类指标以评价样本外结果
knn_pipeline=Pipeline([('scale',StandardScaler()),('knn',KNeighborsClassifier())]) # 导入标准化器以仅在训练样本拟合尺度参数
training_dates=classroom_panel.loc[train_mask,'date'].reset_index(drop=True) # 保存训练样本季度以建立前向折
validation_dates=sorted(training_dates.unique())[-4:] # 事先设定训练期末四个验证季度
forward_splits=[(np.flatnonzero(training_dates.lt(date)),np.flatnonzero(training_dates.eq(date))) for date in validation_dates] # 每折只以更早季度训练并按整季度验证
forward_splits=[split for split in forward_splits if len(split[0]) and len(split[1])] # 移除无训练或无验证样本的折
if len(forward_splits)<3: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查') # 前向验证折不足时停止
grid_search=GridSearchCV(knn_pipeline,{'knn__n_neighbors':[3,5,7,9,11,13]},cv=forward_splits) # 让折内缩放和KNN严格按时间调参
grid_search.fit(x_train_raw.reset_index(drop=True),y_train.reset_index(drop=True)) # 不让最终时间外测试参与调参
predicted_labels=grid_search.predict(x_test_raw); majority_baseline=max(y_test.mean(),1-y_test.mean()) # 按主体分组交叉验证选择邻居数
classification_metrics={'accuracy':accuracy_score(y_test,predicted_labels),'precision':precision_score(y_test,predicted_labels),'recall':recall_score(y_test,predicted_labels)} # 生成时间外分类预测
adoption_conditions_met=classification_metrics['accuracy']-majority_baseline>=.03 and classification_metrics['precision']>=.85 and classification_metrics['recall']>=.85 # 合并分离度、稳定性与最小簇占比检查要求
print(grid_search.best_params_,confusion_matrix(y_test,predicted_labels),majority_baseline,classification_metrics,adoption_conditions_met) # 合并分离度、稳定性与最小簇占比检查要求[[TN,FP],[FN,TP]]、多数类基线、样本外准确率/精度/召回与判断条件。另行核对:逐公司检查标签 shift(-1) 不跨公司,并用多数类常数预测手算基线。[商业大数据分析与应用]